import os
import logging
import openpyxl
from docx import Document
from datetime import datetime, timedelta
from docx.shared import Pt
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
from docx.oxml.shared import qn
from docx.oxml import OxmlElement
import re
from pypinyin import lazy_pinyin

# 配置logging
logging.basicConfig(
    level=logging.INFO,
    format='%(lineno)d - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('请假系统.log', mode='w', encoding='utf-8'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)
# ==================================================
# 配置常量定义
# ==================================================
# 默认行高设置（对应1.02厘米）
DEFAULT_ROW_HEIGHT = 578  # 单位值，约等于1.02厘米

# 文件路径配置
FILE_PATHS = {
    "duty_roster": "值班表.xlsx",
    "contacts": "通讯录.xlsx",
    "leave_template": "请假表模板.docx",
    "output_doc": "自动生成的请假文档.docx"
}

# 需要请假的时间段
TARGET_TIME_SLOT = "17:50-20:20"


# ==================================================
# 日期处理模块
# ==================================================
class DateProcessor:
    @staticmethod
    def get_current_date_info():
        """获取当前日期和下一周的日期范围"""
        today = datetime.now()
        current_weekday = today.weekday()  # 0是周一，6是周日
        
        # 计算下周一的日期
        days_to_next_monday = 7 - current_weekday if current_weekday > 0 else 0
        next_monday = today + timedelta(days=days_to_next_monday)
        
        # 生成下周的日期范围（周一到周日）
        next_week_dates = []
        for i in range(7):
            date = next_monday + timedelta(days=i)
            next_week_dates.append(date)
        
        return today, next_week_dates


# ==================================================
# Excel数据读取模块
# ==================================================
class ExcelReader:
    @staticmethod
    def get_duty_staff(duty_roster_path):
        """从值班表中获取目标时间段的所有值班人员"""
        duty_staff = []
        
        try:
            wb = openpyxl.load_workbook(duty_roster_path, data_only=True)
            sheet = wb.active
            
            # 查找目标时间段的行
            target_time_slot_row = ExcelReader._find_target_time_slot_row(sheet)
            
            if target_time_slot_row is None:
                logger.warning("没有找到时间段信息")
                return []
            
            # 检测目标时间段单元格合并了多少行
            merged_rows_count = ExcelReader._get_merged_rows_count(sheet, target_time_slot_row)
            logger.info(f"目标时间段 '{TARGET_TIME_SLOT}' 占用了 {merged_rows_count} 行")
            
            # 获取目标时间段所有行中的值班人员
            duty_staff = ExcelReader._extract_duty_staff(sheet, target_time_slot_row, merged_rows_count)
            
        except Exception as e:
            logger.error(f"读取值班表时出错: {e}")
        
        logger.info(f"收集到的值班人员列表: {duty_staff}")
        return duty_staff
    
    @staticmethod
    def _find_target_time_slot_row(sheet):
        """查找目标时间段所在的行"""
        # 首先尝试直接查找目标时间段
        for row in range(1, sheet.max_row + 1):
            #cell_value是表格的值
            cell_value = sheet.cell(row=row, column=1).value
            if cell_value and TARGET_TIME_SLOT in str(cell_value):
                return row
        
        # 如果没找到目标时间段，就找最后一个时间段
        for row in range(1, sheet.max_row + 1):
            cell_value = sheet.cell(row=row, column=1).value
            if cell_value and "时间段" in str(cell_value):
                return row
                
        logger.error(f"未找到包含 '{TARGET_TIME_SLOT}' 的时间段")
        return None
    
    @staticmethod
    def _get_merged_rows_count(sheet, target_row):
        """获取合并单元格包含的行数"""
        merged_rows_count = 1
        for merged_cell in sheet.merged_cells.ranges:
            if (merged_cell.min_row <= target_row <= merged_cell.max_row and 
                merged_cell.min_col <= 1 <= merged_cell.max_col):
                merged_rows_count = merged_cell.max_row - merged_cell.min_row + 1
                break
        return merged_rows_count
    
    @staticmethod
    def _extract_duty_staff(sheet, target_row, merged_rows_count):
        """从工作表中提取值班人员信息"""
        duty_staff = []
        
        for row_offset in range(merged_rows_count):
            current_row = target_row + row_offset
            
            for col in range(2, sheet.max_column + 1):
                staff_name = sheet.cell(row=current_row, column=col).value
                if staff_name:
                    # 存储值班人姓名和对应的星期几（0-6，对应周一到周日）
                    weekday = col - 2  # 假设第2列是周一
                    # 避免重复添加同一个人
                    if not any(person["姓名"] == staff_name and person["星期几"] == weekday for person in duty_staff):
                        duty_staff.append({"姓名": staff_name, "星期几": weekday})
        
        return duty_staff
    
    @staticmethod
    def get_staff_details(contacts_path, duty_staff_list):
        """从通讯录中获取值班人员的详细信息，找不到的人员会自动跳过"""
        # 使用字典存储人员信息，键为姓名，值为包含学号、学院和星期几列表的字典
        staff_dict = {}
        not_found_count = 0
        
        try:
            wb = openpyxl.load_workbook(contacts_path, data_only=True)
            sheet = wb.active
            
            # 确定各列的索引
            name_col, student_id_col, college_col = ExcelReader._find_contact_columns(sheet)
            
            if not all([name_col, student_id_col, college_col]):
                logger.warning("通讯录格式不符合预期，未找到必要的列")
                return []
            
            # 构建人员信息字典
            staff_dict, not_found_count = ExcelReader._build_staff_dict(
                sheet, duty_staff_list, name_col, student_id_col, college_col
            )
            
            if not_found_count > 0:
                logger.warning(f"总共有{not_found_count}名人员信息在通讯录中未找到")
            
        except Exception as e:
            logger.error(f"读取通讯录时出错: {e}")
        
        # 转换字典为列表返回
        return list(staff_dict.values())
    
    @staticmethod
    def _find_contact_columns(sheet):
        """查找通讯录中各列的索引"""
        name_col = student_id_col = college_col = None
        for col in range(1, sheet.max_column + 1):
            header = sheet.cell(row=3, column=col).value
            if header:
                if "姓名" in str(header):
                    name_col = col
                elif "学号" in str(header):
                    student_id_col = col
                elif "学院" in str(header) or "部门" in str(header):
                    college_col = col
        return name_col, student_id_col, college_col
    
    @staticmethod
    def _build_staff_dict(sheet, duty_staff_list, name_col, student_id_col, college_col):
        """构建值班人员详细信息字典"""
        staff_dict = {}
        not_found_count = 0
        
        for staff in duty_staff_list:
            name = staff["姓名"]
            found = False
            
            for row in range(2, sheet.max_row + 1):
                if sheet.cell(row=row, column=name_col).value == name:
                    student_id = sheet.cell(row=row, column=student_id_col).value
                    college = sheet.cell(row=row, column=college_col).value
                    
                    # 如果是第一次遇到该人员，初始化信息
                    if name not in staff_dict:
                        staff_dict[name] = {
                            "姓名": name,
                            "学号": student_id,
                            "学院": college,
                            "星期几列表": [staff["星期几"]]  # 存储多个星期几
                        }
                    else:
                        # 如果已经存在该人员，只添加新的星期几（避免重复）
                        if staff["星期几"] not in staff_dict[name]["星期几列表"]:
                            staff_dict[name]["星期几列表"].append(staff["星期几"])
                    
                    found = True
                    break
            
            if not found:
                not_found_count += 1
                logger.error(f"在通讯录中未找到{name}的信息，跳过...")
        
        return staff_dict, not_found_count


# ==================================================
# Word文档处理模块
# ==================================================
class WordFormatter:
    @staticmethod
    def set_cell_vertical_alignment(cell, alignment="center"):
        """设置单元格的垂直对齐方式"""
        # 获取单元格的XML元素
        tc = cell._tc
        tcPr = tc.get_or_add_tcPr()
        
        # 移除现有的垂直对齐设置（如果有）
        for element in tcPr.iterchildren():
            if element.tag.endswith('vAlign'):
                tcPr.remove(element)
                break
        
        # 创建新的垂直对齐元素
        vAlign = OxmlElement('w:vAlign')
        vAlign.set(qn('w:val'), alignment)
        tcPr.append(vAlign)
    
    @staticmethod
    def set_row_height(row, height):
        """使用新方法设置表格行的高度"""
        try:
            # 直接使用docx库的API设置行高
            row.height = height
            
            # 获取行的XML元素
            tr = row._tr
            trPr = tr.get_or_add_trPr()
            
            # 删除现有的行高设置
            for element in list(trPr):
                if element.tag.endswith('trHeight'):
                    trPr.remove(element)
            
            # 创建新的行高元素
            trHeight = OxmlElement('w:trHeight')
            trHeight.set(qn('w:val'), str(height))
            trHeight.set(qn('w:hRule'), 'exact')  # 精确行高
            trPr.append(trHeight)
            
            return True
        except Exception as e:
            logger.error(f"设置行高时出错: {e}")
            return False
    
    @staticmethod
    def set_cell_format(cell, text):
        """设置单元格的文本、字体和格式"""
        # 清空单元格内容
        for paragraph in cell.paragraphs:
            for run in paragraph.runs:
                run.text = ""
        
        # 如果单元格中没有段落，添加一个
        if not cell.paragraphs:
            cell.add_paragraph()
        
        # 获取第一个段落
        paragraph = cell.paragraphs[0]
        paragraph.text = ""  # 清空段落文本
        
        # 分离中文和非中文（数字和英文）
        # 匹配中文字符
        chinese_pattern = re.compile(r'[\u4e00-\u9fff]+')
        # 匹配非中文字符（数字、英文等）
        non_chinese_pattern = re.compile(r'[^\u4e00-\u9fff]+')
        
        # 查找所有中文和非中文部分
        text_parts = []
        last_end = 0
        
        # 先找出所有中文部分
        for match in chinese_pattern.finditer(text):
            # 如果中文前有非中文，先添加非中文部分
            if match.start() > last_end:
                text_parts.append(('non_chinese', text[last_end:match.start()]))
            # 添加中文部分
            text_parts.append(('chinese', match.group()))
            last_end = match.end()
        
        # 如果最后还有非中文部分，添加它
        if last_end < len(text):
            text_parts.append(('non_chinese', text[last_end:]))
        
        # 如果没有匹配到任何内容，添加整个文本
        if not text_parts:
            text_parts.append(('non_chinese' if non_chinese_pattern.match(text) else 'chinese', text))
        
        # 为每个部分添加不同字体的run
        for part_type, part_text in text_parts:
            run = paragraph.add_run(part_text)
            
            # 设置字体大小为五号（约10.5磅）
            run.font.size = Pt(10.5)
            
            if part_type == 'chinese':
                # 中文使用仿宋_GB2312
                run.font.name = '仿宋_GB2312'
                run._element.rPr.rFonts.set(qn('w:eastAsia'), '仿宋_GB2312')
            else:
                # 数字和英文使用Times New Roman
                run.font.name = 'Times New Roman'
                run._element.rPr.rFonts.set(qn('w:ascii'), 'Times New Roman')
                run._element.rPr.rFonts.set(qn('w:hAnsi'), 'Times New Roman')
        
        # 设置段落水平居中对齐
        paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
        
        # 设置单元格垂直居中对齐
        WordFormatter.set_cell_vertical_alignment(cell, "center")


# ==================================================
# 请假文档生成模块
# ==================================================
class LeaveDocumentGenerator:
    """请假文档生成器"""
    
    @staticmethod
    def _update_document_date(doc, next_monday_date):
        """更新文档右下角的日期为下周一的日期，确保数字使用Times New Roman字体"""
        # 拆分日期为数字和中文部分，用于分别设置字体
        year = str(next_monday_date.year)
        month = str(next_monday_date.month)
        day = str(next_monday_date.day)
        
        # 查找文档中的最后一个段落
        if len(doc.paragraphs) > 0:
            last_paragraph = doc.paragraphs[-1]
            
            # 使用正则表达式查找日期格式
            date_pattern = r'\d{4}年\d{1,2}月\d{1,2}日'
            if re.search(date_pattern, last_paragraph.text):
                # 保存原始段落的对齐方式
                original_alignment = last_paragraph.alignment
                
                # 清除段落中的所有内容
                for _ in range(len(last_paragraph.runs)):
                    last_paragraph.runs[0]._element.getparent().remove(last_paragraph.runs[0]._element)
                
                # 替换文本为新日期，分开设置数字和中文的字体
                # 年前面的数字
                year_run = last_paragraph.add_run(year)
                year_run.font.name = 'Times New Roman'
                year_run.font.size = Pt(10.5)
                
                # 添加"年"字
                year_char = last_paragraph.add_run("年")
                year_char.font.name = '仿宋_GB2312'
                year_char._element.rPr.rFonts.set(qn('w:eastAsia'), '仿宋_GB2312')
                year_char.font.size = Pt(10.5)
                
                # 月前面的数字
                month_run = last_paragraph.add_run(month)
                month_run.font.name = 'Times New Roman'
                month_run.font.size = Pt(10.5)
                
                # 添加"月"字
                month_char = last_paragraph.add_run("月")
                month_char.font.name = '仿宋_GB2312'
                month_char._element.rPr.rFonts.set(qn('w:eastAsia'), '仿宋_GB2312')
                month_char.font.size = Pt(10.5)
                
                # 日前面的数字
                day_run = last_paragraph.add_run(day)
                day_run.font.name = 'Times New Roman'
                day_run.font.size = Pt(10.5)
                
                # 添加"日"字
                day_char = last_paragraph.add_run("日")
                day_char.font.name = '仿宋_GB2312'
                day_char._element.rPr.rFonts.set(qn('w:eastAsia'), '仿宋_GB2312')
                day_char.font.size = Pt(10.5)
                
                # 恢复原始对齐方式
                last_paragraph.alignment = original_alignment
                
            else:
                # 如果没有找到日期格式，在最后一段添加日期并设置右对齐
                from docx.enum.text import WD_ALIGN_PARAGRAPH
                last_paragraph.alignment = WD_ALIGN_PARAGRAPH.RIGHT
                
                # 添加机构名称
                run = last_paragraph.add_run("武汉科技大学学生工作处\n")
                run.font.name = '仿宋_GB2312'
                run._element.rPr.rFonts.set(qn('w:eastAsia'), '仿宋_GB2312')
                run.font.size = Pt(10.5)
                
                # 拆分日期中的中文和数字，分别设置字体
                # 年前面的数字
                year_run = last_paragraph.add_run(year)
                year_run.font.name = 'Times New Roman'
                year_run.font.size = Pt(10.5)
                
                # 添加"年"字
                year_char = last_paragraph.add_run("年")
                year_char.font.name = '仿宋_GB2312'
                year_char._element.rPr.rFonts.set(qn('w:eastAsia'), '仿宋_GB2312')
                year_char.font.size = Pt(10.5)
                
                # 月前面的数字
                month_run = last_paragraph.add_run(month)
                month_run.font.name = 'Times New Roman'
                month_run.font.size = Pt(10.5)
                
                # 添加"月"字
                month_char = last_paragraph.add_run("月")
                month_char.font.name = '仿宋_GB2312'
                month_char._element.rPr.rFonts.set(qn('w:eastAsia'), '仿宋_GB2312')
                month_char.font.size = Pt(10.5)
                
                # 日前面的数字
                day_run = last_paragraph.add_run(day)
                day_run.font.name = 'Times New Roman'
                day_run.font.size = Pt(10.5)
                
                # 添加"日"字
                day_char = last_paragraph.add_run("日")
                day_char.font.name = '仿宋_GB2312'
                day_char._element.rPr.rFonts.set(qn('w:eastAsia'), '仿宋_GB2312')
                day_char.font.size = Pt(10.5)
    
    @staticmethod
    def generate_leave_document(leave_doc_template_path, output_doc_path, staff_info_list, next_week_dates):
        """将人员信息填写到请假文档中"""
        try:
            # 打开原始文档
            doc = Document(leave_doc_template_path)
            
            # 查找表格
            if len(doc.tables) == 0:
                logger.error("文档中未找到表格")
                return False
            
            table = doc.tables[0]  # 假设第一个表格是需要填写的表格
            
            # 设置行高（1.02厘米 ≈ 28.8磅）
            row_height = 578  # 约1.02厘米
            
            # 处理表格行数
            LeaveDocumentGenerator._adjust_table_rows(table, len(staff_info_list))
            
            # 填写表格内容
            LeaveDocumentGenerator._fill_table_content(table, staff_info_list, next_week_dates)
            
            # 设置所有行的高度
            for i, row in enumerate(table.rows):
                WordFormatter.set_row_height(row, row_height)
            
            # 更新文档右下角的日期为下周一的日期
            LeaveDocumentGenerator._update_document_date(doc, next_week_dates[0])
            
            # 保存文档
            doc.save(output_doc_path)
            logger.info(f"请假文档已生成: {output_doc_path}")
            return True
        
        except Exception as e:
            logger.error(f"填写请假文档时出错: {e}")
            return False
    
    @staticmethod
    def _adjust_table_rows(table, staff_count):
        """调整表格行数以适应人员数量"""
        required_rows = staff_count + 1  # +1 是为了表头
        
        # 如果行数过多，删除多余的行（保留表头）
        while len(table.rows) > required_rows:
            # 删除最后一行
            table._tbl.remove(table.rows[-1]._tr)
        
        # 如果表格行数不足，添加新行
        while len(table.rows) < required_rows:
            table.add_row()
    
    @staticmethod
    def _fill_table_content(table, staff_info_list, next_week_dates):
        """填写表格内容"""
        # 对人员信息进行排序：先按学院拼音排序，相同学院按学号排序
        sorted_staff_list = sorted(staff_info_list, key=lambda x: (
            lazy_pinyin(x["学院"] if x["学院"] else ""),  # 先按学院拼音排序（None值处理为空字符串）
            x["学号"] if x["学号"] else ""   # 相同学院按学号排序（None值处理为空字符串）
        ))
        
        # 打印排序后的人员列表，便于调试
        logger.debug("排序后的人员列表:")
        for staff in sorted_staff_list:
            logger.debug(f"  {staff['学院']} - {staff['学号']} - {staff['姓名']}")
        
        for i, staff in enumerate(sorted_staff_list, start=1):  # 从第2行开始填写（跳过表头）
            # 获取对应的请假日期列表（不显示年份）
            leave_dates = []
            for weekday in staff["星期几列表"]:
                # 修改日期格式，不显示年份，并去除一位数日期的前导0
                # 使用%#m和%#d在Windows上可以去除前导0
                leave_date = next_week_dates[weekday].strftime("%#m月%#d日")
                leave_dates.append(leave_date)
            
            # 将多个日期用顿号连接成一行
            combined_dates = "、".join(leave_dates)
            
            # 填写各列数据，并设置格式
            row = table.rows[i]
            # 确保行有足够的单元格，如果不足则跳过该行
            if len(row.cells) >= 4:  # 确保有足够的列
                # 使用辅助函数设置单元格格式
                WordFormatter.set_cell_format(row.cells[0], str(staff["学号"]) if staff["学号"] else "")
                WordFormatter.set_cell_format(row.cells[1], staff["姓名"])
                WordFormatter.set_cell_format(row.cells[2], staff["学院"] if staff["学院"] else "")
                WordFormatter.set_cell_format(row.cells[3], combined_dates)
            else:
                logger.error(f"警告：第{i+1}行单元格数量不足，无法完全填写数据")

# ==================================================
# 主程序入口
# ==================================================
def main():
    # 1. 获取当前日期和下周日期
    today, next_week_dates = DateProcessor.get_current_date_info()
    logger.info(f"当前日期: {today.strftime('%Y-%m-%d')}")
    logger.info(f"下周日期范围: {next_week_dates[0].strftime('%Y-%m-%d')} 到 {next_week_dates[6].strftime('%Y-%m-%d')}")
    
    # 2. 获取值班表中目标时间段的值班人员
    logger.info(f"正在查找时间段 '{TARGET_TIME_SLOT}' 的值班人员")
    duty_staff = ExcelReader.get_duty_staff(FILE_PATHS["duty_roster"])
    logger.info(f"在目标时间段找到 {len(duty_staff)} 名值班人员")
    
    # 3. 从通讯录获取详细信息（自动跳过找不到的人员）
    detailed_info = ExcelReader.get_staff_details(FILE_PATHS["contacts"], duty_staff)
    logger.info(f"成功获取 {len(detailed_info)} 名值班人员的详细信息")
    
    # 4. 填写请假文档
    success = LeaveDocumentGenerator.generate_leave_document(
        FILE_PATHS["leave_template"],
        FILE_PATHS["output_doc"],
        detailed_info,
        next_week_dates
    )
    
    if success:
        logger.info("请假文档生成成功！")
    else:
        logger.error("生成请假文档失败，请检查错误信息。")


if __name__ == "__main__":
    main()